/* * Fabric3 * Copyright (c) 2009-2015 Metaform Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fabric3.gradle.plugin.itest.impl; import javax.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import org.eclipse.aether.RepositorySystem; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.artifact.DefaultArtifact; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryPolicy; import org.eclipse.aether.resolution.ArtifactResolutionException; import org.eclipse.aether.resolution.DependencyResolutionException; import org.fabric3.api.host.Fabric3Exception; import org.fabric3.api.host.Names; import org.fabric3.api.host.classloader.DelegatingResourceClassLoader; import org.fabric3.api.host.classloader.MaskingClassLoader; import org.fabric3.api.host.contribution.ContributionService; import org.fabric3.api.host.contribution.ContributionSource; import org.fabric3.api.host.contribution.FileContributionSource; import org.fabric3.api.host.domain.Domain; import org.fabric3.api.host.monitor.DestinationRouter; import org.fabric3.api.host.runtime.HiddenPackages; import org.fabric3.api.host.util.IOHelper; import org.fabric3.gradle.plugin.api.test.IntegrationTests; import org.fabric3.gradle.plugin.api.test.IntegrationTestsFactory; import org.fabric3.gradle.plugin.api.test.TestRecorder; import org.fabric3.gradle.plugin.api.test.TestResult; import org.fabric3.gradle.plugin.api.test.TestSuiteResult; import org.fabric3.gradle.plugin.itest.config.TestPluginConvention; import org.fabric3.gradle.plugin.itest.deployer.GradleDeployer; import org.fabric3.gradle.plugin.itest.report.JUnitReportWriterImpl; import org.fabric3.gradle.plugin.itest.resolver.AetherBootstrap; import org.fabric3.gradle.plugin.itest.resolver.ProjectDependencies; import org.fabric3.gradle.plugin.itest.runtime.GradleRuntimeBooter; import org.fabric3.gradle.plugin.itest.runtime.PluginDestinationRouter; import org.fabric3.gradle.plugin.itest.stopwatch.NoOpStopWatch; import org.fabric3.gradle.plugin.itest.stopwatch.StopWatch; import org.fabric3.gradle.plugin.itest.stopwatch.StreamStopWatch; import org.fabric3.plugin.Fabric3PluginException; import org.fabric3.plugin.api.runtime.PluginRuntime; import org.fabric3.plugin.resolver.Resolver; import org.fabric3.plugin.runtime.PluginBootConfiguration; import org.fabric3.plugin.runtime.PluginConstants; import org.fabric3.plugin.util.ClassLoaderHelper; import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.logging.Logger; import org.gradle.api.tasks.TaskAction; import org.gradle.internal.service.ServiceRegistry; import org.gradle.logging.ProgressLogger; import org.gradle.logging.ProgressLoggerFactory; import org.gradle.logging.StyledTextOutput; import org.gradle.logging.StyledTextOutputFactory; /** * Boots an embedded Fabric3 runtime and runs integration tests for the current project and other configured projects. */ public class Fabric3TestTask extends DefaultTask { private static final String FABRIC3_GRADLE = "org.fabric3.gradle"; private ProgressLoggerFactory progressLoggerFactory; private StyledTextOutput output; private JUnitReportWriterImpl reportWriter; private StopWatch stopWatch; @Inject public Fabric3TestTask(ProgressLoggerFactory progressLoggerFactory, StyledTextOutputFactory outputFactory) { this.progressLoggerFactory = progressLoggerFactory; this.output = outputFactory.create("fabric3"); reportWriter = new JUnitReportWriterImpl(); if (Boolean.parseBoolean(System.getProperty("fabric3.performance"))) { stopWatch = new StreamStopWatch("gradle", TimeUnit.MILLISECONDS, System.out); } else { stopWatch = new NoOpStopWatch(); } } @TaskAction public void fabric3Test() throws Fabric3Exception, Fabric3PluginException { stopWatch.start(); ProgressLogger progressLogger = progressLoggerFactory.newOperation("fabric3"); progressLogger.setDescription("Fabric3 tests"); progressLogger.setLoggingHeader("Fabric3 tests"); progressLogger.started("BOOTING"); Logger logger = getLogger(); Project project = getProject(); TestPluginConvention convention = (TestPluginConvention) project.getConvention().getByName(TestPluginConvention.FABRIC3_TEST_CONVENTION); boolean offline = project.getGradle().getStartParameter().isOffline(); RepositorySystem system = AetherBootstrap.getRepositorySystem(); ServiceRegistry registry = getServices(); RepositorySystemSession session = AetherBootstrap.getRepositorySystemSession(system, registry, offline); RepositoryPolicy repoPolicy = new RepositoryPolicy(convention.isRemoteRepositoryEnabled(), convention.getUpdatePolicy(), RepositoryPolicy.CHECKSUM_POLICY_WARN); RepositoryPolicy snapshotPolicy = new RepositoryPolicy(convention.isRemoteSnapshotRepositoryEnabled(), convention.getSnapshotUpdatePolicy(), RepositoryPolicy.CHECKSUM_POLICY_WARN); List<RemoteRepository> repositories = AetherBootstrap.getRepositories(registry, repoPolicy, snapshotPolicy); Resolver resolver = new Resolver(system, session, repositories, convention.getRuntimeVersion()); PluginBootConfiguration configuration = createBootConfiguration(convention, resolver, system, session); GradleRuntimeBooter booter = new GradleRuntimeBooter(configuration); stopWatch.split("Gradle setup"); PluginRuntime runtime = booter.boot(); stopWatch.split("Fabric3 boot"); String environment = runtime.getHostInfo().getEnvironment(); logger.info("Fabric3 started [Environment: " + environment + "]"); progressLogger.progress("BOOTED"); ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); boolean aborted = false; IntegrationTests integrationTests = null; try { Thread.currentThread().setContextClassLoader(configuration.getBootClassLoader()); // load the contributions deployContributions(runtime, convention, resolver); stopWatch.split("Fabric3 deploy contributions"); File buildDirectory = project.getBuildDir(); String namespace = convention.getCompositeNamespace(); String name = convention.getCompositeName(); GradleDeployer deployer = new GradleDeployer(namespace, name, buildDirectory, logger); String errorText = convention.getErrorText(); aborted = !deployer.deploy(runtime, errorText); if (aborted) { return; } stopWatch.split("Fabric3 deploy test composite"); progressLogger.progress("Running Fabric3 tests"); IntegrationTestsFactory integrationTestsFactory = runtime.getComponent(IntegrationTestsFactory.class); integrationTests = integrationTestsFactory.createTests(progressLogger); integrationTests.execute(); stopWatch.split("Fabric3 run tests"); tryLatch(runtime); stopWatch.stop(); stopWatch.flush(); } finally { try { booter.shutdown(); Thread.currentThread().setContextClassLoader(oldClassLoader); } catch (Exception e) { // ignore } } if (aborted) { progressLogger.completed("ABORTED"); throw new Fabric3PluginException("Integration tests were aborted."); } else { processResults(integrationTests, progressLogger, convention.isReport()); } } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") private void processResults(IntegrationTests integrationTests, ProgressLogger progressLogger, boolean report) throws Fabric3PluginException { TestRecorder recorder = integrationTests.getRecorder(); if (report) { writeReport(recorder); } if (recorder.hasFailures()) { for (TestSuiteResult suiteResult : recorder.getResults()) { for (TestResult result : suiteResult.getTestResults()) { if (result.getType() != TestResult.Type.FAILED) { continue; } output.text("\n" + result.getTestClassName() + " > " + result.getTestMethodName()); output.withStyle(StyledTextOutput.Style.Failure).println(" FAILED"); output.withStyle(StyledTextOutput.Style.Error).println(result.getThrowable()); } } displaySummary(recorder); progressLogger.completed("FAILED"); throw new Fabric3PluginException("There were failing integration tests."); } else { displaySummary(recorder); progressLogger.completed("COMPLETED"); } } @SuppressWarnings("ResultOfMethodCallIgnored") private void writeReport(TestRecorder recorder) throws Fabric3PluginException { File buildDir = getProject().getBuildDir(); File reportsDir = new File(buildDir, "reports"); File outputDir = new File(reportsDir, "integration-tests"); outputDir.mkdirs(); try (FileOutputStream stream = new FileOutputStream(new File(outputDir, "tests.xml"))) { reportWriter.write(recorder, stream); } catch (IOException e) { throw new Fabric3PluginException(e); } } private void displaySummary(TestRecorder recorder) { int successfulTests = recorder.getSuccessfulTests(); int failedTests = recorder.getFailedTests(); String test = successfulTests == 1 ? "test" : "tests"; output.println("\n" + successfulTests + " " + test + " succeeded, " + failedTests + " failed\n"); } /** * Waits on a latch component if one is configured for the test run. * * @param runtime the runtime */ private void tryLatch(PluginRuntime runtime) { Object latchComponent = runtime.getComponent(Object.class, PluginConstants.TEST_LATCH_SERVICE); if (latchComponent != null) { Class<?> type = latchComponent.getClass(); try { Method method = type.getDeclaredMethod("await"); getLogger().lifecycle("Waiting on Fabric3 runtime latch"); method.invoke(latchComponent); getLogger().lifecycle("Fabric3 runtime latch released"); } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException | NoSuchMethodException | SecurityException e) { getLogger().error("Exception attempting to wait on latch service", e); } } } /** * Resolves and deploys configured contributions. * * @param runtime the runtime */ private void deployContributions(PluginRuntime runtime, TestPluginConvention convention, Resolver resolver) throws Fabric3PluginException { Set<Artifact> contributions = convention.getContributions(); ContributionService contributionService = runtime.getComponent(ContributionService.class, Names.CONTRIBUTION_SERVICE_URI); Domain domain = runtime.getComponent(Domain.class, Names.APPLICATION_DOMAIN_URI); List<ContributionSource> sources = new ArrayList<>(); if (!contributions.isEmpty()) { try { Set<URL> resolved = resolver.resolve(contributions); createSource(sources, resolved); } catch (ArtifactResolutionException e) { throw new Fabric3PluginException("Error installing contributions", e); } } Set<File> fileContributions = convention.getFileContributions(); if (!fileContributions.isEmpty()) { for (File file : fileContributions) { URI uri = URI.create(file.getName()); try { ContributionSource source = new FileContributionSource(uri, file.toURI().toURL(), -1, true); sources.add(source); } catch (MalformedURLException e) { throw new GradleException(e.getMessage(), e); } } } // deploy the archive and URL-based contributions try { List<URI> uris = contributionService.store(sources); contributionService.install(uris); domain.include(uris); } catch (Fabric3Exception e) { throw new Fabric3PluginException("Error installing contributions", e); } Set<Project> projectContributions = convention.getProjectContributions(); if (projectContributions.isEmpty()) { return; } // deploy project contributions List<ContributionSource> projectSources = new ArrayList<>(); for (Project project : projectContributions) { ContributionSource source = ProjectDependencies.createSource(project); projectSources.add(source); } try { List<URI> uris = contributionService.store(projectSources); contributionService.install(uris); domain.include(uris); } catch (Fabric3Exception e) { throw new GradleException(e.getMessage(), e); } } private void createSource(List<ContributionSource> sources, Set<URL> resolved) { for (URL url : resolved) { URI uri = URI.create(new File(url.getFile()).getName()); ContributionSource source = new FileContributionSource(uri, url, -1, true); sources.add(source); } } /** * Creates the configuration to boot the Maven runtime, including resolving dependencies. * * @return the boot configuration */ private PluginBootConfiguration createBootConfiguration(TestPluginConvention convention, Resolver resolver, RepositorySystem system, RepositorySystemSession session) { Project project = getProject(); configureWeb(convention); try { Set<Artifact> shared = convention.getShared(); Set<Project> sharedProjects = convention.getSharedProjects(); Set<Artifact> extensions = convention.getExtensions(); Set<Artifact> profiles = convention.getProfiles(); Set<Artifact> hostArtifacts = resolver.resolveHostArtifacts(shared); Set<Artifact> runtimeArtifacts = resolver.resolveRuntimeArtifacts(); Artifact testExtension = new DefaultArtifact(FABRIC3_GRADLE, "test-extension", "jar", convention.getRuntimeVersion()); extensions.add(testExtension); List<ContributionSource> runtimeExtensions = resolver.resolveRuntimeExtensions(extensions, profiles); Set<URL> moduleDependencies = ProjectDependencies.calculateProjectDependencies(project, hostArtifacts, resolver); ClassLoader parentClassLoader = createParentClassLoader(); URL[] sharedUrls = getSharedUrls(hostArtifacts, sharedProjects); ClassLoader hostClassLoader = new DelegatingResourceClassLoader(sharedUrls, parentClassLoader); ClassLoader bootClassLoader = ClassLoaderHelper.createBootClassLoader(hostClassLoader, runtimeArtifacts); PluginBootConfiguration configuration = new PluginBootConfiguration(); configuration.setBootClassLoader(bootClassLoader); configuration.setHostClassLoader(hostClassLoader); DestinationRouter router = new PluginDestinationRouter(getLogger()); configuration.setRouter(router); configuration.setExtensions(runtimeExtensions); configuration.setModuleDependencies(moduleDependencies); File buildDir = project.getBuildDir(); configuration.setOutputDirectory(buildDir); if (convention.getSystemConfig() != null) { configuration.setSystemConfig(convention.getSystemConfig()); } else if (convention.getSystemConfigFile() != null) { InputStream is = new FileInputStream(convention.getSystemConfigFile()); ByteArrayOutputStream os = new ByteArrayOutputStream(); IOHelper.copy(is, os); configuration.setSystemConfig(new String(os.toByteArray())); } configuration.setRepositorySession(session); configuration.setRepositorySystem(system); configuration.setBuildDir(project.getBuildDir()); return configuration; } catch (DependencyResolutionException | ArtifactResolutionException | IOException e) { throw new GradleException(e.getMessage(), e); } } private URL[] getSharedUrls(Set<Artifact> shared, Set<Project> sharedProjects) throws MalformedURLException { Set<URL> sharedUrls = new HashSet<>(); for (Artifact artifact : shared) { if (artifact.getFile() == null) { throw new GradleException("Archive not found for shared project: " + artifact.getGroupId() + ":" + artifact.getArtifactId()); } sharedUrls.add(artifact.getFile().toURI().toURL()); } for (Project sharedProject : sharedProjects) { sharedUrls.add(ProjectDependencies.findArtifact(sharedProject).toURI().toURL()); } return sharedUrls.toArray(new URL[sharedUrls.size()]); } private void configureWeb(TestPluginConvention convention) { Set<Artifact> extensions = convention.getExtensions(); Set<Artifact> profiles = convention.getProfiles(); for (Artifact extension : extensions) { if (extension.getArtifactId().equals("fabric3-jetty")) { return; } } for (Artifact profile : profiles) { String id = profile.getArtifactId(); if (id.equals("profile-ws") || id.equals("profile-rs") || id.equals("profile-web")) { extensions.add(new DefaultArtifact(profile.getGroupId(), "fabric3-jetty", "jar", profile.getVersion())); return; } } } private ClassLoader createParentClassLoader() { ClassLoader parentClassLoader = getClass().getClassLoader(); String[] hidden = HiddenPackages.getPackages(); if (hidden.length > 0) { // mask hidden JDK and system classpath packages parentClassLoader = new MaskingClassLoader(parentClassLoader, hidden); } return parentClassLoader; } }